Chapter 30
MFC and DirectX

by Kenn Scribner

In This Chapter

  What Is DirectX? 1078
  DirectX Basics 1081
  DirectDraw Basics 1083
  Using DirectDraw in Practice 1102

Many aspects of application development set the truly professional job apart from the rest. One aspect that is surprisingly easy to exploit is the addition of colorful, fast graphics, sound, auxiliary input devices, and such. In this chapter, you’ll explore the basics of Microsoft’s multimedia extension library, DirectX, and you’ll examine in particular DirectDraw, which targets graphics. Let’s begin with a brief history lesson.

What Is DirectX?

Not so many years ago, Microsoft released the Game SDK in response to claims that Windows didn’t offer game developers a viable platform for high-performance game execution. In fact, game developers had a point—Windows wasn’t a good platform for high frame rate animation. Windows was (and is) a wonderful platform for software development because it provides you, the developer, with a platform-independent architecture. By and large, you are unconcerned with the specifics of any individual system. You program to the idealized Windows model, and Windows will handle the specific system details for you.

But platform independence always comes at a cost, and in this case (to the game developers, anyway), it was throttled animation frame rates. That is, the Windows GDI simply couldn’t update the screen as fast as game developers required. Their games, therefore, were slow and sluggish, and Windows was pretty much abandoned as a game platform. Developers instead turned to DOS as their architecture of choice for the hottest games of the day (remember Doom?)

Of course, this was at a time in history when Microsoft was trying to wean the computer market off DOS. They had to invent some mechanism for high-performance animation (and later input and sound) if they wanted the game community to abandon DOS in favor of Windows. The Game SDK was Microsoft’s initial answer. Today, the Game SDK has grown into the DirectX family of developer APIs. I won’t be teaching game programming in this chapter. Instead, I’ll concentrate on explaining DirectX technology. I’ll specifically describe techniques to use DirectDraw (a major piece of DirectX) from within your own applications to enhance your application’s capabilities and feature sets.


Tip:  

Don’t believe for a moment that DirectX is only suited for game programming. Because of its performance-minded approach, it is an ideal technological choice for adding multimedia features to any application.


DirectX in a Nutshell

DirectX is a COM-based system that transcends both driver-level and application-level software layers. The main DirectX design goal was to be fast while still offering a measure of device independence. Microsoft achieved this by implementing the architecture you see in Figure 30.1. Here, you have the choice of using the full-flavored Windows API, or you can take the leaner and meaner DirectX path.


Figure 30.1  The DirectX architecture.

If you look specifically at the DirectX portion of Figure 30.1, you see two main components: the HAL and the HEL. HAL, or Hardware Abstraction Layer (a term no doubt borrowed from Windows NT) is responsible for determining the capabilities of the local hardware and offering them to you in a device-independent fashion. For those capabilities DirectX offers but that aren’t supported by the local hardware configuration, the HEL, or Hardware Emulation Layer, takes over. On any given system, DirectX uses a blend of HAL and HEL functionality, though you do have the option of forcing DirectX to use only HEL functions. This would present your application with a worst-case performance scenario, which is well suited for development. If your application performs well with little to no special hardware support, it will perform well no matter what the configuration in the field.

DirectX Components

DirectX provides you with several components optimized to work with their respective subsystem. The first DirectX component, and probably the most well-known, is DirectDraw. DirectDraw provides you with the mechanisms you require to bypass the Windows GDI in favor of faster bitmap data transfers and the resulting higher animation frame rates. But drawing functions alone weren’t enough for the gaming community, so Microsoft developed DirectSound, DirectInput, DirectPlay, and DirectSetup. There is also another component added more recently than the rest—Direct3D.

DirectSound enables you to transcend the Win32 PlaySound() API call by giving you access to hardware-based (or -emulated) sound mixing capability with the added feature of 3D sound emulation. Sound mixing enables you to generate a single sound stream from many simultaneous (stored) sound inputs. This clearly has benefit to the game development community, as games frequently require multiple sound tracks. The added benefit of directional sound is especially intriguing.

DirectInput is used to gather input device feedback, such as when using a keyboard, a joystick, or some other tracking device. And starting with DirectX Version 5, you not only receive inputs from a device, but you can also send outputs to the device to simulate force applied against the input device. This is known as force feedback. This would enable you to run a flight simulation on your computer and feel the yoke push back during simulated high-G maneuvers. Many game developers find this feature exciting, as it adds a tremendous amount of realism to their simulations and games.

DirectPlay adds the multiuser perspective to your application. If you’ve ever seen online games, you know the game participants all want to annihilate each other, and it’s much more satisfying to them when they blow away a live player’s character versus a computer-generated one. But gaming issues aside, imagine the possibilities for your application if you enable your users to connect to each other and collaborate. DirectPlay makes this quite easy to implement.

DirectSetup is the mechanism you use to install DirectX on systems that don’t currently have DirectX capability. This currently includes Windows 95 installations only, however. Windows 98 is shipped with DirectX (upgrades may be possible when available, though) and Windows NT computers require DirectX to be installed through NT Service Packs only (security being a primary concern here).

Direct3D is built upon DirectDraw, so it uses many of the primitives DirectDraw provides you. It offers an alternative to other 3D programming APIs, including OpenGL. Inside Direct3D, you’ll find the basic 3D programming support as well as some high-end 3D technology you don’t commonly see in other PC-based APIs (high-end texture mapping, for example).

Given this high-level tour of DirectX, it’s time to see how you add this compelling technology to your applications. I’ll start with some basic concepts you need to begin working with DirectX. Then, I’ll discuss DirectDraw in detail, which is the oldest and largest of the DirectX components.

DirectX Basics

The fundamental architecture that binds the DirectX family of components together is COM, and though you don’t need to be a COM wizard to use DirectX from within your applications, you need to understand how COM works (if only at a high level) and be familiar with COM-specific coding practices. There are also some terms and concepts I should introduce. I’ll start with the necessary COM information that varies from Chapter 10, “COM.”



Differences in COM Programming Style and DirectX

When you program DirectX components, you will be dealing directly with COM components you (somehow) created and initialized. The first major difference you’ll note when you program using DirectX is that you also combine traditional API library-based programming with your COM programming. Perhaps this is best illustrated by example.

Let’s say you want to do some work with DirectDraw, as you’ll do shortly. If you follow the traditional COM approach, you would expect to be given a CLSID and an IID, which, when used with CoCreateInstance() would a create new DirectDraw object. This method works just fine. As it happens, though, you can also call the DirectX function DirectDrawCreate(), which in turn calls CoCreateInstance() on your behalf and returns to you the DirectDraw COM object. Although this is not unheard of in COM programming circles, it is somewhat different. Don’t be fooled into thinking you are not working with a COM object! You just obtained a new object using a different means. All the COM rules still apply.


Note:  

If you do create the DirectDraw object using the traditional CoCreateInstance() approach, you will need to call IDirectDraw::Initialize() before you actually use the object’s capabilities. This is unnecessary when creating the object using DirectDrawCreate().



Caution:  

Do not be deceived. You are dealing with COM objects when you program using DirectX, so follow all the COM rules no matter how you obtained your objects initially. Any quality COM text should provide you with the information you need to follow the rules of COM if you are unsure.


The question most often asked at this point is where did DirectDrawCreate() come from? The answer is that the DirectX SDK ships with both debug and retail libraries you must link against to use DirectX. In the case of DirectDraw, you will link with ddraw.lib and possibly dxguid.lib. Other DirectX components require additional (or different) libraries. The DirectX component creation functions are certainly there, though there are other useful functions exported by the libraries as well. When in doubt, check with the online help files for both Developer Studio and the DirectX SDK.


Note:  

Visual C++ version 6 ships with the development tools you’ll need to work with DirectX 6. You might still require the latest DirectX drivers, however. These can be obtained at the Microsoft DirectX Internet site free of charge. See http://www.microsoft.com/directx for download details.


Another minor variation is that the DirectX components all return custom function return and error codes. A good example of this involves the enumeration functions. I’ll discuss this process further in a moment, but in most cases when using a DirectX component, you first (generally) enumerate the possible drivers, and then activate the component with the desired driver. The enumeration callback functions all return a Boolean value. The odd part is the enumeration functions return DDENUMRET_OK and DDENUMRET_CANCEL for TRUE and FALSE respectively.

DirectX Component Startup Sequence

Because I’ve mentioned driver enumerations, it’s time to look more closely at the general sequence of events when creating a DirectX application. Here are the steps you normally follow to activate DirectX:

  Enumerate the supported drivers.
  Either ask the user or programmatically determine which driver you should use in this specific case.
  Create the component using the driver GUID you obtained from the enumeration process.

I use the term drivers loosely here. DirectPlay, for example, uses an enumeration of connection shortcuts to determine which connection is required for multiuser contacts. When you are creating a DirectDraw object, on the other hand, you literally enumerate through the installed display device drivers and select the one most appropriate for your application. I’ll make this more concrete when I discuss DirectDraw in the next section.

By the way, much of what you see regarding DirectDraw applies to the other DirectX components, at least as far as general mechanics are concerned. You initialize them in a similar manner, check for errors, and in general integrate them into your application in very much the same way you will DirectDraw. When you see how DirectDraw is applied, you should find using the other DirectX technologies easily applied also.

DirectDraw Basics

The greatest benefit DirectDraw provides you is the means to access the given computer system’s hardware in the leanest and meanest possible manner. You won’t be coding video registers yourself, but the performance you realize using DirectDraw can be almost as stunning.

You can imagine DirectDraw as a replacement for certain parts of the standard Windows GDI (see Chapter 4, “Painting, Device Contexts, Bitmaps, and Fonts”). Although it won’t algorithmically generate rectangles for you like the GDI, because DirectDraw doesn’t actually draw anything in the GDI sense, it will put an image you provide onscreen very quickly. And unlike GDI, DirectDraw offers several special effects, such as enhanced stretching and transparency.

I’ll start discussing DirectDraw by showing you how it’s initialized, but I’ll move on to other DirectDraw basic concepts later in the chapter and in this chapter’s sample programs.

Initializing DirectDraw

In most cases, the very first thing you’ll do when using DirectDraw is to determine what video output formats the given computer system supports. You’ll see a notable exception to this later in the chapter when I discuss windowed DirectDraw. Normally, however, most DirectDraw applications will want to use the entire screen for rendering their output. This being the case, you will need to know what video modes the system offers so you might select the most optimum for your needs. If the optimum choice isn’t available, you can gracefully downgrade to a secondary video mode you can still use effectively.

DirectDraw driver enumeration is done in much the same way you enumerate windows (using EnumWindows()). You call an API function, DirectDrawEnumerate(), in this situation and provide it a callback function. The callback function records the installed video drivers for later use. This code invokes DirectDrawEnumerate() for enumeration:

if ( FAILED(DirectDrawEnumerate((LPDDENUMCALLBACK)EnumDDrawDevices,
                                                   (LPVOID)this))) {
   AfxMessageBox(“Unable to enumerate DirectDraw devices.”,
                   MB_OK|MB_ICONERROR);
   // Handle error...;
} // if

The parameters to DirectDrawEnumerate() are simply the address of the enumeration function and an arbitrary 32-bit parameter, which in the preceding case is a this pointer to the C++ class performing the enumeration.

The enumeration function itself has no requirement to do anything special, but you’ll typically want to record the driver presented to the callback function each time the callback function is executed. If you build a selection list using the callback function, you’ll know what is available to you when the enumeration has been completed. This chapter’s first sample program, DXMode, uses the code you see in Listing 30.1 to build such a selection list.



Listing 30.1 DXMode DirectDraw Driver Enumeration


BOOL WINAPI EnumDDrawDevices( GUID FAR *lpGUID,
                              LPSTR lpDriverDescription,
                              LPSTR lpDriverName,
                              LPVOID lpContext ) {
   // Cast your pointer
   CDXModeDlg* pDialog = reinterpret_cast<CDXModeDlg*>(lpContext);
   int iIndex = pDialog->m_CDevices.AddString(lpDriverDescription);

   // Add the device to the devices combo box
   if ( iIndex != LB_ERR ) {
      // You’ve added the string representing the device,
      // so you now create a GUID and store it with the
      // item.
      GUID* guidDevice = NULL;
      if ( lpGUID != NULL ) {
         // If you have a device GUID, create storage
         // for a copy, then copy.
         guidDevice = new GUID;
         *guidDevice = *lpGUID; // copy
      } // if
      // Set the item’s data
      pDialog->m_CDevices.SetItemData(iIndex,
                  reinterpret_cast<DWORD>(guidDevice));
   } // if
   else {
      // Return an error
      return DDENUMRET_CANCEL;
   }

   return DDENUMRET_OK;
}

There are two interesting things to note regarding Listing 30.1. First, the this pointer passed as a parameter in DirectDrawEnumerate() is known as the context parameter in the enumeration function parameter list. Because DXMode is really a dialog box-based application, the this pointer I passed was the MFC CDialog-based class CDXModeDlg. After I recast it back to a CDXModeDlg pointer, I can add the driver’s textual description, lpDriverDescription, to a combo box contained within the dialog box. This is how I save the driver descriptions for later presentation to the user.

The second interesting feature is that the drivers are really identified by GUID (a unique 128-bit COM object identifier). It’s true you are given a textual description of the driver. But when you actually want to do anything with the driver, you have to provide its GUID identifier. I store this with the driver name in the combo box (I add the GUID as item data). When you select a driver and mode from the dialog’s combo box, I retrieve the item data and cast it to a GUID pointer. From there, I access the driver itself.

At this time, you have a complete list of driver descriptions and GUID identifiers. This in itself is interesting, as you now know what drivers are currently loaded on the given computer. You could at this time load new or updated drivers, for example.

But what you will most likely do now is also enumerate the video modes the drivers support. For example, your application might have been designed to work with a 320×240 screen (a common gaming standard, but certainly not a standard Windows video mode). After you enumerate the supported video modes, you’ll know whether you’ll be able to use your application’s native mode (320×240) or be forced into using another video mode (which will almost certainly introduce stretching artifacts when the screen is rendered).

But before you are able to determine the modes you can work with, you’ll need to initialize DirectDraw with the driver GUID you obtained from the enumeration process. This code creates the DirectDraw object using the DirectDrawCreate() function I mentioned previously:

LPDIRECTDRAW pIDirectDraw = NULL;
if ( FAILED(DirectDrawCreate(lpGUID,&pIDirectDraw,NULL)) ) {
   AfxMessageBox(“Error creating DirectDraw object”,
                  MB_OK | MB_ICONERROR);
   // Handle error...
} // if

Note that the first parameter to DirectDrawCreate() is a pointer to a GUID. The GUID you provide is the GUID associated with the driver you want to use (which, of course, is why I enumerated the driver list to begin with).


Note:  

You do not need to enumerate the drivers and modes, but it is common practice. If your application is not intended for the more esoteric video modes, you can skip the driver enumeration and pass NULL as the driver GUID to DirectDrawCreate(). This will create a DirectDraw object that uses the default system driver.


If DirectDrawCreate() was successful, it will return to you an IDirectDraw interface to the DirectDraw COM object. When you have this interface, you can set the cooperative level you plan to use. This is an application-wide setting that essentially tells DirectDraw how it should react to other Windows applications, and even to Windows itself. For example, you tell DirectDraw you want to use the full screen by using the DDSCL_EXCLUSIVE and DDSCL_FULLSCREEN cooperative level settings. With these particular settings, when your application has focus, the screen is yours to render as you require—other applications won’t interfere.


Note:  

You must set the cooperative level before calling other DirectDraw methods. Many will fail outright if the cooperative level has not been set.


Table 30.1 lists the available cooperative level bits as well as their effect upon the system and other applications. Note that some are mutually exclusive, whereas others must be used in pairs.

Table 30.1 DirectDraw Cooperative Level Settings

Setting Meaning/Use

DDSCL_FULLSCREEN Application will use the full display screen (GDI can be ignored, must be used with DDSCL_EXCLUSIVE).
DDSCL_ALLOWREBOOT Allow Ctrl+Alt+Delete while in full-screen exclusive mode.
DDSCL_NOWINDOWCHANGES Prevents DirectDraw from modifying the application window state.
DDSCL_NORMAL Application will work as a regular Windows application (cannot be used with DDSCL_EXCLUSIVE and DDSCL_FULLSCREEN).
DDSCL_EXCLUSIVE Exclusive access (must be used with DDSCL_FULLSCREEN).
DDSCL_ALLOWMODEX Application can handle displays using non-standard Windows display modes.
DDSCL_SETFOCUSWINDOW Window will receive focus messages.
DDSCL_SETDEVICEWINDOW Window is associated with the DirectDraw object and will cover the screen in full-screen mode (Windows 98/2000 only).
DDSCL_CREATEDEVICEWINDOW Create a window to be associated with the DirectDraw object (Windows 98/2000 only).

You can set the cooperative level with IDirectDraw::SetCooperativeLevel(), using the following code:

if ( FAILED(m_pIDirectDraw->SetCooperativeLevel(m_hWnd,
                                        DDSCL_FULLSCREEN |
                                        DDSCL_EXCLUSIVE |
                                        DDSCL_NOWINDOWCHANGES)) ) {
   AfxMessageBox(“Error setting cooperative level”,
                  MB_OK | MB_ICONERROR);
   // Handle error
} // if

With the cooperative level set, you can now enumerate the video modes supported by the driver you’ve selected. You perform the enumeration using IDirectDraw::EnumDisplayModes(), as shown here:

if ( FAILED(pIDirectDraw->EnumDisplayModes(0,NULL,
                     reinterpret_cast<LPVOID>(this),
                     (LPDDENUMMODESCALLBACK)EnumDeviceModes)) ) {
   AfxMessageBox(“Error enumerating modes”,
                 MB_OK | MB_ICONERROR);
   // Handle error
} // if



Although the function signature for video mode enumeration is different than the API call used to enumerate the driver list, the same basic information is here. That is, you pass in a this pointer as a context value and a pointer to an enumeration function. Again using DXMode as an example, the device mode enumeration function is shown in Listing 30.2.

Listing 30.2 DXMode Device Video Mode Enumeration


BOOL WINAPI EnumDeviceModes( LPDDSURFACEDESC lpDDSurfaceDesc,
                              LPVOID lpContext )
{
   // Cast your pointer
   CDXModeDlg* pDialog = reinterpret_cast<CDXModeDlg*>(lpContext);

   // Add this mode to the listbox
   CString strMode;
   strMode.Format(“%dx%dx%d, refresh %d”,
                    lpDDSurfaceDesc->dwWidth,
                    lpDDSurfaceDesc->dwHeight,
                    lpDDSurfaceDesc->ddpfPixelFormat.dwRGBBitCount,
                    lpDDSurfaceDesc->dwRefreshRate);

   int iIndex = pDialog->m_CModes.AddString(strMode);
   if ( iIndex != LB_ERR ) {
      // Add the surface descriptor to the item’s data
      LPDDSURFACEDESC lpDesc = new DDSURFACEDESC;
      memcpy(lpDesc,lpDDSurfaceDesc,sizeof(DDSURFACEDESC));
      pDialog->m_CModes.SetItemData(iIndex,
         reinterpret_cast<DWORD>(lpDesc));
   } // if
   else  {
      // Return an error
      return DDENUMRET_CANCEL;
   } // else
 
   return DDENUMRET_OK;
}

Here, the interesting thing to note is that the enumeration is passed in not only a context value (as with driver enumeration), but it is also passed a surface description (more on this in the next section). You can access the surface description to extract the information you need to judge the video mode best suited for your application. In the case of DXMode, the video mode is stored in a list box. When you select the video mode from the list box, DXMode will enable that video mode for you.

At this point, you have selected a DirectDraw driver, created the DirectDraw object, and obtained the information you need to select an appropriate video mode. If your selected video mode requires a palette, you must create one now.

DirectDraw and Palettes

DirectDraw is capable of displaying many video modes, and many of them require palettes. A palette, distilled to its core functionality, is simply a table of color references. That is, a 256-color palette contains 256 table entries. The palettized bitmap is made up of entries into the table. When Windows displays a 256-color bitmap, it looks at the individual table entries in the bitmap and matches them to the color values stored in the palette table. The bitmap doesn’t contain actual color information—it contains palette table indices.

The color information is actually stored in the palette. If you were to change one of the table color values while displaying the bitmap, you would change the appearance of the bitmap onscreen. Thus the secret to palettes is to make sure the palette you want (with the correct color values in its color table) is always used when your bitmap is displayed. If you fail to do this, your bitmap will have unpredictable colors and will almost certainly not appear onscreen as you would like.

Managing palettes, then, is critical to proper DirectDraw operation if you either plan to use a palettized display mode (256 colors, for instance) or you are working with DirectDraw in a window (versus full screen). There are three major steps to accomplish to manage the palette you want Windows to use when your application is active:

1.  Create your palette.
2.  Handle the situation where Windows tells you the system palette has changed (WM_PALETTECHANGED).
3.  Handle the situation where Windows requests from you the palette you want to become the current system palette (WM_QUERYNEWPALETTE).

To create a new palette using DirectDraw, you simply create a table containing the color values you want, and then pass that to DirectDraw. How you create the color values is up to you. You could fill in each of the palette color entries by hand, thus selecting optimum color settings. You could generate the colors algorithmically. And, of course, you could peer inside a Windows device-independent bitmap (DIB) to see what color information is required by that bitmap and assign those colors to your palette. I’ll leave the by-hand method as an exercise for the reader, but the algorithmic and DIB assignment methods require some explanation. You’ll see these methods in action when I discuss this chapter’s third sample program later in the chapter.

Windows will happily use whatever palette you provide, but there is an algorithm that generates a Windows default palette—a palette with the standard Windows color entries. This palette is called a 332 palette for the entries created by the algorithm. I won’t describe the algorithm in detail (that would require a lot of explanation), referring you instead to other Windows programming books (such as Programming Windows 98/NT Unleashed, from Sams Publishing). However, here is the code that executes the algorithm:

// Create a standard 332 palette
PALETTEENTRY ape[256];
for ( int i = 0; i < 256; i++ ) {
    ape[i].peRed   = (BYTE)(((i >> 5) & 0x07) * 255 / 7);
    ape[i].peGreen = (BYTE)(((i >> 2) & 0x07) * 255 / 7);
    ape[i].peBlue  = (BYTE)(((i >> 0) & 0x03) * 255 / 3);
    ape[i].peFlags = (BYTE)0;
} // for

As you can see, the loop assigns values to the red, green, and blue components of the palette color entry structure.

If your application doesn’t use a standard 332 palette, or if you simply want to use whatever palette was stored with your DIB (which might or might not be a 332 palette), you need to dig into your DIB and retrieve the palette color information stored there. To do this, you look at the DIB’s BITMAPINFOHEADER and retrieve the color information. Assuming your bitmap was stored as a resource (versus a disk-based DIB file), this code will retrieve the palette information for you:

// Locate the bitmap resource.  Note this assumes the
// bitmap is stored as a program resource (and not as
// a disk file).
HRSRC hBitmap = NULL;
if ( (hBitmap = ::FindResource(NULL,
                         MAKEINTRESOURCE(idBitmap),
                         RT_BITMAP))) {
   LPBITMAPINFOHEADER lpbi = (LPBITMAPINFOHEADER)::LockResource(
Ä::LoadResource(NULL,hBitmap));
   if ( lpbi != NULL ) {
      // Locate the color information, which is stored past
      // the BITMAPINFOHEADER.
      RGBQUAD* prgb = (RGBQUAD*)((BYTE*)lpbi + lpbi->biSize);

      // Determine how many palettized colors you’re
      // talking about.
      int iNumColors;
      if (lpbi == NULL || lpbi->biSize < sizeof(BITMAPINFOHEADER))
         iNumColors = 0;
      else if (lpbi->biBitCount > 8)
         iNumColors = 0;
      else if (lpbi->biClrUsed == 0)
         iNumColors = 1 << lpbi->biBitCount;
      else
         iNumColors = lpbi->biClrUsed;

      //  Pull the color information
      for( int i = 0; i < iNumColors; i++ ) {
         ape[i].peRed   = prgb[i].rgbRed;
         ape[i].peGreen = prgb[i].rgbGreen;
         ape[i].peBlue  = prgb[i].rgbBlue;
         ape[i].peFlags = 0;
      } // for
   } // if
} // if



In this case you find the bitmap resource, load it, and lock it in memory so you can access its information directly. Because the BITMAPINFOHEADER is the first part of the DIB in memory, after you lock it in place, you have a pointer to the DIB’s information header. Given that, you then determine the type of bitmap by looking at the color information (stored as in-place RGBQUAD structures). If you find the bitmap indeed requires a palette, you assign to the palette entries the information stored in the RGBQUAD structures (which have nearly identical form to the PALETTEENTRY color information structures).

You now have a collection of 256 PALETTEENTRY values, each filled with color information. These will eventually compose the palette’s color table. It’s now time to actually create a palette. Naturally, you do this using DirectDraw:

// Create a new DirectDraw palette
// IDirectDraw* pIDirectDraw (declared previously)
IDirectDrawPalette* pIDDPalette = NULL;
HRESULT hr = pIDirectDraw->CreatePalette(DDPCAPS_8BIT,ape,
                                         &pIDDPalette,NULL);
if ( FAILED(hr) ) {
   // Some error
   AfxMessageBox(“Error creating the DirectDraw palette”,
                 MB_OK | MB_ICONERROR);
   // Handle error
} // if

Windows, through DirectDraw, will now have a copy of the palette you want to use for your application and will assign it to the system palette. Note that this code creates a 256-color palette (established by the DDPCAPS_8BIT setting). There are other types of palettes—see the online help for more information. In any case, after the palette has been established, it’s time to add palette management support.

The first thing to do is add a handler for the WM_PALETTECHANGED message. This message indicates that another application obtained focus and has changed the palette from underneath you. It’s up to you to decide how you want to handle this situation. However, DirectDraw helps you here, too, as you’ll see when I discuss the practical side to using DirectDraw later in the chapter. For now, I’ll simply show you the basic handler code. I’ll fill in some of the DirectDraw details later in the chapter when I discuss the RePalettize() helper method.

The basic WM_PALETTECHANGED handler code looks like this:

void CMyCWnd::OnPaletteChanged(CWnd* pFocusWnd)
{
   // Someone changed the palette...was it you?
   if ( pFocusWnd->GetSafeHwnd() == this->m_hWnd ) {
      // It was you, so let base class handle the
      // message
      CWnd::OnPaletteChanged(pFocusWnd);
   } // if
   else {
      // It wasn’t you, so repalettize
      RePalettize(); // <-- a method you provide...
   } // if
}

It is critical to examine the CWnd pointer passed into OnPaletteChanged(). If you were responsible for changing the palette, you would enter an infinite loop trying to establish a new palette. In effect, you would call yourselves forever.


Caution:  

Always check to see if your window changed the palette before handling the WM_PALETTECHANGED message. You will enter an infinite loop if you don’t.


Windows issues the WM_PALETTECHANGED message when an application, which could be the current application (yours), changes the system palette. Similarly, Windows will request from you a palette to use as the system palette when your application gains focus. This message, WM_QUERYNEWPALETTE, is Windows’s way of asking you to re-establish your palette. The basic handler for WM_QUERYNEWPALETTE looks like this:

BOOL CTableView::OnQueryNewPalette()
{
   // You now control the palette, so repalettize
   return RePalettize();// <-- a method you provide...
}

If you change the palette in response to WM_QUERYNEWPALETTE, your handler returns TRUE. If not, it returns FALSE. Fortunately, you can use the same method to repalettize when using DirectDraw, as you’ll see shortly.

Now that the palette management is in place, it’s time to create a primary DirectDraw surface and possibly several secondary surfaces you might use as back buffers or for bitmap storage.

DirectDraw Surfaces

DirectDraw can do many things for you, as you’ve seen if you’ve tried DXMode. But the goal of DirectDraw is and always has been to provide you with the fastest, highest-performance bitmap data transfers possible. After all, when you display a bitmap on the computer’s video screen, what you are really doing is copying bitmap color information from somewhere in the computer’s main memory to the computer’s video memory (advanced video hardware notwithstanding). When the information is there, the video hardware reads the video memory and actually displays the information.

The essence of this transaction is that data was copied from one memory location to another. The faster the data is copied from place to place, the faster your overall frame rate will be. The frame rate refers to the number of completed cycles your application can perform in one second. If your application is able to update the screen 30 times a second, you have a frame rate of 30 frames per second. Most PC applications run in the 10-15 frame per second range, with some of the better-performing games reaching into the 20-frame-per-second range.

Aside from calculations, which can take a significant amount of time to complete, the bitmap block transfer is typically the frame rate-limiting factor. Bitmap block transfers, or blit for short, are critical for smooth animation, simply because your eye can easily perceive breaks in the frames at somewhere around 10 frames per second. Frame rates in this range are usually considered poor for quality animation.

The DirectDraw surface is designed to minimize the blit latency, or in other words, move the data from here to there in the least amount of time possible. If you understand device contexts (see Chapter 4), you know that a device context contains bitmap information. Perhaps the bitmap will be rendered onscreen, or just as likely, a printer will render the bitmap. DirectDraw surfaces perform the same function (though not to printers). Unlike device contexts, DirectDraw surfaces do not access the Windows GDI directly. The means you can’t draw ellipses, text, or polygons on a DirectDraw surface using GDI methods. For that, you request a device context from the DirectDraw surface using IDirectDrawSurface::GetDC() and work with GDI using that device context, not via the surface itself (don’t forget to release the device context afterwards!). But DirectDraw surfaces are like device contexts in that they manage blitting, or the transfer of bitmap data from one location to another. It’s time now to see how surfaces are created.



Creating DirectDraw Surfaces

When you use DirectDraw, the first surface you will most likely create is your primary surface. This is akin to a Windows screen device context. Whatever bitmap you place on this surface will be rendered onscreen. When you have a primary surface, you then create secondary surfaces, of which there are two kinds. The first is a surface designated to simply contain a bitmap. You obtain a bitmap, from your resources or from a disk file, and load it into the surface. When the bitmap is there, it can be transferred quickly to the primary surface or another secondary surface. The other type of secondary surface is a back buffer. This is a surface used to assemble your frame, like a scratch pad. You use the back buffer as a staging area where you create the frame, and then blit the back buffer to the primary surface for viewing.

The first step is to create the primary surface. To do this, you use DirectDraw’s IDirectDraw::CreateSurface() method, like this:

DDSURFACEDESC ddsd;
ZeroMemory(&ddsd,sizeof(ddsd));
ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_CAPS;
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE;

// IDirectDraw* pIDirectDraw (declared previously)
IDirectDrawSurface* pDDSPrimary = NULL;
HRESULT hr = pIDirectDraw->CreateSurface(&ddsd,&pDDSPrimary,NULL);
if ( FAILED( hr ) ) {
   // Some error
   AfxMessageBox(“Error creating primary surface”,
                 MB_OK | MB_ICONERROR);
   // Handle error
} // if

Assuming CreateSurface() was successful, you now have the means to display bitmap information onscreen.

Creating secondary surfaces uses the same mechanism, but with the DDSURFACEDESC structure completed slightly differently:

DDSURFACEDESC ddsd;
ZeroMemory(&ddsd,sizeof(ddsd));
ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH;
ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;
ddsd.dwWidth = cx;
ddsd.dwHeight = cy;

// IDirectDraw* pIDirectDraw (declared previously)
IDirectDrawSurface* pDDSSecondary = NULL;
HRESULT hr = pIDirectDraw->CreateSurface(&ddsd,&pDDSSecondary,NULL);
if ( FAILED(hr) ) {
   // Some error
   AfxMessageBox(“Error creating primary surface”,
                 MB_OK | MB_ICONERROR);
   // Handle error
} // if

DirectDraw creates a primary surface and associates it with the video display when you set the DDSCAPS_PRIMARYSURFACE bit in the DDSURFACEDESC ddsCaps field. Conversely, DirectDraw creates a secondary surface when you apply the DDSCAPS_OFFSCREENPLAIN bit in the dwCaps field. Another detail is that DirectDraw knows how big a primary surface needs to be—the size of your video output. For secondary surfaces, however, you must specify how large the surface is required to be, in pixels. You do this by setting the DDSD_HEIGHT and DDSD_HEIGHT bits in the dwFlags field, which tells DirectDraw the dwWidth and dwHeight fields in the DDSURFACEDESC structure are valid.

Transferring Bitmaps from Surface to Surface

Now that you have DirectDraw surfaces that you can work with, you load them with data and move the data around as required by your application. As I mentioned previously, many applications create a primary surface for display, a secondary surface for staging (the back buffer), and any number of secondary surfaces destined to contain bitmaps to be used as animated sprites (moving objects), backgrounds, and a myriad of other uses. A typical scenario would involve copying a sprite to the back buffer, then displaying the back buffer by copying the back buffer to the primary surface.

To reiterate, this bitmap transfer operation is termed a blit, for bitmap block transfer. Blitting data from one surface to another is a simple matter using DirectDraw, as you might expect. After all, it was designed for this purpose. You simply call the IDirectDrawSurface::Blt() method:

// IDirectDrawSurface* pDDSSecondary,
//                     pDDSSomeSurface (both declared previously)
CRect rcDest; // filled with valid destination location info
CRect rcSrc; // filled with valid source location info (may be NULL)
hr = pDDSSecondary->Blt(rcDest,pDDSSomeSurface,rcSrc,
                        DDBLT_WAIT,NULL);
if ( FAILED( hr ) ) {
   // Some error
   AfxMessageBox(“Error bltting to secondary surface”,
                 MB_OK | MB_ICONERROR);
   // Handle error
} // if

In this example, the bitmap information contained in the pDDSSomeSurface surface will be copied to the pDDSSecondary surface. You first fill the destination rectangle with the location where you want to place the information in the target surface. The source rectangle may also be completed, or you can pass NULL to Blt() to copy the entire source surface contents to the target surface.


Caution:  

Unlike blitting to device contexts, DirectDraw is unforgiving when it comes to rectangles that extend outside the bounds of a surface. The rectangle locations you pass to Blt() must contain coordinates that exist within the surface’s bounds. You will receive a DDERR_INVALIDRECT error if the rectangular region is out of a surface boundary.



Note:  

DirectDraw automatically stretches bitmaps to fit. If the source and destination rectangles differ in height and width, the resulting bitmap will be accordingly distorted.


Clearing DirectDraw Surfaces

Frequently, you will need to clear a secondary surface to some predetermined color as initialization prior to blitting bitmap data. This is easily accomplished using DirectDraw special effects combined with the IDirectDrawSurface::Blt() method you just saw:

DDBLTFX ddbltfx;
ddbltfx.dwSize = sizeof(ddbltfx);
ddbltfx.dwFillColor = 0; // black
// IDirectDrawSurface* pDDSSecondary (declared previously)
HRESULT hr = m_lpDDSSecondary->Blt(NULL,NULL,NULL,
                                   DDBLT_COLORFILL | DDBLT_WAIT,
                                   &ddbltfx);
if ( FAILED( hr ) ) {
   // Some error
   AfxMessageBox(“Error clearing secondary surface”,
                 MB_OK | MB_ICONERROR);
   // Handle error
} // if

Note the differences between this Blt() call and an actual bitmap transfer. Both the source and destination rectangles are NULL (though the destination rectangle need not be), the source surface is NULL, and I added the DDBLT_COLORFILL option along with a pointer to the completed DDBLTFX structure. If the destination rectangle is non-NULL and contains valid location information, only that area of the target surface will be filled with the fill color instead of the entire surface.

DirectDraw Page Flipping

A related concept to DirectDraw surfaces is page flipping. Until DirectDraw, many Windows animation-based applications (not just games) suffered from tearing. Tearing is a phenomenon you will see when the animation begins during one monitor refresh cycle but finishes during the next. That is, the images you see on a color display are created by electron guns shooting beams of electrons at the back surface of the monitor’s glass face. (Of course, this assumes a cathode ray tube; you will see the same effects using a liquid crystal display even though the mechanics of the display are different.) The inside of the monitor’s front glass surface is coated with phosphors that, when excited by the electron beam, glow red, green, or blue. The phosphors are actually separated (red, green, and blue), but because they are so small, your eye combines their respective color values. Your perception is of a single color, which is the blending of the red, green, and blue color values of the individual pixels.



The electron beam starts at the top of the screen, moves horizontally, then vertically, until it reaches the bottom of the screen. At that time, the electron beam must be deactivated and routed back to the top of the screen to scan the monitor’s glass surface yet again. When the electron beam is deactivated, the monitor is said to be in vertical retrace, or vertical blanking. When the monitor is about to retrace, the video card issues a hardware interrupt called the vertical blanking signal.

As most professional game programmers know, the vertical retrace period is quite lengthy as compared to the processing speed of today’s microprocessors. So, it makes sense to coordinate lengthy game calculations and bitmap data transfers with the monitor’s vertical blanking signal. When the monitor is retracing, you can’t display any new information anyway, so you might as well use the time to create the next image the video hardware will display.

Windows normally prevents you from intercepting the vertical blank signal, so you were faced with making these lengthy calculations and blits at random times. Because of this, the image being displayed on the screen might be updated at any time, including when the monitor is scanning the image. When this happens, the upper part of the image is scanned using information from one animation frame while the lower part of the screen is scanned using information from the next frame. If the frames were different, the image you would see would straddle the two frames and thus appear to be torn (the tearing phenomenon). If you’ve tried this chapter’s second sample program, the tearing demo, you’ve seen this in action.

To allow developers the ability to manage the vertical blank signal, DirectDraw was introduced. This is a core DirectDraw feature, and Microsoft had to provide such a solution if game development (or any serious animation work) on a Windows platform was to be taken seriously. How DirectDraw manages the vertical blank signal is by page flipping. The page in page flipping refers to video hardware of yore that implemented non-contiguous (segmented) video memory banks. Each bank was called a page. The term has persisted even if the hardware has improved significantly.

When you use DirectDraw page flipping, you begin by creating a complex DirectDraw surface (this surface is very much analogous to the OpenGL double-buffered rendering context described in Chapter 29, “MFC and OpenGL”). The complex surface will appear to you to be two surfaces, though it is in reality considered a single surface. You simply copy whatever bitmap information you require to the back buffer surface, and DirectDraw will display it when triggered. When DirectDraw senses the vertical blank signal, it swaps the primary for the secondary. You tell DirectDraw when you are ready for this swap operation to take place by triggering DirectDraw.

Let’s begin with creating the complex surface. To do this, you again use IDirectDraw::CreateSurface(), but you complete the DDSURFACEDESC structure slightly differently:

// Typically 1 unless there is hardware support
#define NUMBUFFERS  1

DDSURFACEDESC ddsd;
ZeroMemory(&ddsd,sizeof(ddsd));
ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE |
                      DDSCAPS_FLIP |
                      DDSCAPS_COMPLEX |
                      DDSCAPS_VIDEOMEMORY;
ddsd.dwBackBufferCount = NUMBUFFERS;

// IDirectDraw* pIDirectDraw (declared previously)
IDirectDrawSurface* pDDSprimary = NULL;
HRESULT hr = pIDirectDraw->CreateSurface(&ddsd,& pDDSPrimary,NULL);
if ( FAILED( hr ) ) {
   // Some error
   AfxMessageBox(“Error creating primary surface”,
                  MB_OK | MB_ICONERROR);
   // Handle error
} // if

In this case, when you request the primary surface, you ask for a number of back buffers (DDSD_BACKBUFFERCOUNT), flipping capability (DDSCAPS_FLIP), a complex surface (DDSCAPS_COMPLEX), and that the surface reside in video memory for higher performance (DDSCAPS_VIDEOMEMORY).

If DirectDraw can support this (which it can for at least a single back buffer in emulation mode), you then request access to the back buffer:

DDSCAPS ddscaps;
ddscaps.dwCaps = DDSCAPS_BACKBUFFER;
// IDirectDrawSurface* pDDSprimary (declared previously)
HRESULT hr = pDDSPrimary->GetAttachedSurface(&ddscaps,
                                             &m_lpDDSSecondary);
if ( FAILED( hr ) ) {
   // Some error
   AfxMessageBox(“Error retrieving secondary back buffer”.
                 MB_OK | MB_ICONERROR);
   // Handle error
} // if

As you can see, retrieving the secondary surface is very different from what you saw previously. This is an artifact of the complex surface. Without page flipping, you create both surfaces, primary and secondary. With page flipping, you create a complex primary surface with contains a secondary surface. Therefore, the mechanics of obtaining the secondary surface are naturally a bit different.


Tip:  

Video memory is a limited resource, so create your primary surface and request your secondary surface before you create any other secondary surfaces to contain bitmaps. This should prevent resource contention errors when creating the primary surface.



Caution:  

Be sure to reverse the release order of the surface objects. With page flipping active, the secondary surface (back buffer) must be released prior to releasing the primary surface. This is the opposite of what is required when not using page flipping.


With the surfaces created, you work with the secondary back buffer as you normally would (clear it, copy bitmaps, and so on). When you have completed filling the back buffer, you trigger the page swap by using the IDirectDrawSurface::Flip() method:

// IDirectDrawSurface* pDDSprimary (declared previously)
HRESULT hr = pDDSPrimary->Flip(NULL,DDFLIP_WAIT);
if ( FAILED( hr ) ) {
   // Some error
   AfxMessageBox(“Error flipping page”,
                 MB_OK | MB_ICONERROR);
   // Handle error
} // if

Here, the DDFLIP_WAIT setting tells DirectDraw to swap the primary and secondary surfaces when it receives the next vertical blanking interrupt.

DirectDraw from a Window

Many applications use DirectDraw using the DDSCL_EXCLUSIVE and DDSCL_FULLSCREEN styles. These styles are great for you, the developer, because you don’t need to worry about interacting with other Windows applications. You own the screen when your application is active, so there is no need to worry about painting over another application’s window.



This is definitely not true if you want to use DirectDraw from within a window. In fact, it becomes considerably more complicated because your window might have other windows overlapping it when displayed to the user. Part of your window might be obscured, and part might not be. A DirectDraw primary surface, however, has no compassion for other windows in the system. It only knows it is free to access the entire screen. When your application calls upon DirectDraw to update the screen, it will happily overwrite any other window that might happen to be onscreen.


Note:  

Page flipping is not available to you when using DirectDraw from within a window. Page flipping can only be used in full-screen, exclusive mode.


Of course, this type of behavior is discouraged in normal practice (to put it mildly). To solve this problem, the designers of DirectDraw provide a clipping object. In graphical programming terms, clipping refers to rendering your image only where you are supposed to render. Regions where you are not to render are clipped. Your image will not be rendered there, thus preventing your application from overpainting any other window.

Determining where your application may render and where it may not can get quite complicated, as there may be many overlapping windows. DirectDraw solves this problem by providing a special object designed just to handle this situation. The DirectDraw clipping object, called the clipper, dynamically manages all the relevant clip regions for you. You simply create the clipper, provide it with your window’s handle, and assign the clipper to the primary surface. DirectDraw takes it from there.

Creating the clipper object is similar to creating other DirectDraw objects—you call an IDirectDraw method, which in this case is CreateClipper():

// Since you’re windowed, you need to create a clipper.
// IDirectDraw* pIDirectDraw (declared previously)
IDirectDrawClipper* pDDClipper;
HRESULT hr = pIDirectDraw->CreateClipper(0,&pDDClipper,NULL);
if ( FAILED( hr ) ) {
   // Some error
   AfxMessageBox(“Error creating the clipper”,
                 MB_OK | MB_ICONERROR);
   // Handle error
} // if

After the clipper object has been created, you must provide it with your window’s handle (presumably so it can subclass your window). You do this using the IDirectDrawClipper::SetHWnd() method:

// You now associate the clipper with your frame
// so Windows will update the clipping region for
// you.
CMainFrame* pFrame = (CMainFrame*)AfxGetMainWnd();
ASSERT(pFrame);
HRESULT hr = pDDClipper->SetHWnd(0,pFrame->GetSafeHwnd());
if ( FAILED( hr ) ) {
   // Some error
   AfxMessageBox(“Error setting the clipper\’s HWND”,
                 MB_OK | MB_ICONERROR);
   // Handle error
} // if

When the clipper has your application window’s handle, it is prepared to manage any clipping issues that might arise during your application’s lifetime. However, you still must associate the clipper object with your application’s primary surface:

// Associate your clipper with the primary surface
// IDirectDrawSurface* pDDSPrimary
HRESULT hr = pDDSPrimary->SetClipper(pDDClipper);
if ( FAILED( hr ) ) {
   // Some error
   AfxMessageBox(“Error associating the clipper with the
Äprimary surface”,
                 MB_OK | MB_ICONERROR);
   // Handle error
} // if

When the primary surface has a copy of the clipper object’s interface pointer, you’re free to Release() your pointer (the primary surface will AddRef() it on its own):

// Release our copy of the clipper
pDDClipper->Release();
pDDClipper = NULL;

Now that you’ve seen how DirectDraw is initialized, how surfaces are created, how palettes are managed, and how to handle clipping issues in windowed applications, it’s time to turn to the practical issues surrounding the use of DirectDraw.

Using DirectDraw in Practice

So far, you’ve seen the basic mechanics of using DirectDraw. But there is a practical side, too. For one thing, the surfaces you’ve created need to be initialized with bitmaps. For another, if your application uses DirectDraw from within a window, you must manage the drawing rectangle. And, of course, you must devise a cogent mechanism for dealing with DirectDraw errors, as any DirectDraw method could return a failed result code. And finally, you can’t simply release your DirectDraw interface pointers in any order when your application quits. There is a certain order that must be followed to avoid access violations as parts of DirectDraw terminate. I’ll begin with the relationship between surfaces and bitmaps.

Surfaces and Bitmaps

Like a device context, a DirectDraw surface contains a bitmap. Perhaps the bitmap will be rendered directly, or maybe the surface is simply holding the bitmap for later use. In any case, you must have some method for loading the bitmap into a surface in the first place. And, if you remember from earlier in this chapter, I mentioned I’d revisit the topic of DirectDraw and Windows palettes. First, I’ll explain how to load a bitmap, where I’ll create some reusable code for palette management.

Loading Bitmaps into Surfaces

Loading bitmaps is actually a fairly simple process, when explained. Realize, though, that the method I present here assumes the bitmaps are loaded as resources. Loading bitmaps from disk follows nearly the same process when the bitmap is in memory.

Before you load a bitmap into a DirectDraw surface, you must have created a surface. Earlier in the chapter I provided some simplistic code to do just this, but as with any demonstration code, you must add more to provide for a more robust and practical method. If you examine this chapter’s BlackJack sample program, you’ll find the helper method shown in Listing 30.3 in TableViewDDraw.cpp.

Listing 30.3 Practical DirectDraw Secondary Surface Creation


HRESULT CTableView::CreateSecondarySurface(int cx, int cy,
ÄIDirectDrawSurface** ppDDS)
{
   // Check our pointer
   ASSERT(ppDDS != NULL);
   if ( ppDDS == NULL ) {
      // NULL input pointer...
      return E_POINTER;
   } // if

   // Complete the surface description structure
   DDSURFACEDESC ddsd;
   ZeroMemory(&ddsd,sizeof(ddsd));
   ddsd.dwSize = sizeof(ddsd);
   ddsd.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH;
   ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;
   ddsd.dwWidth = cx;
   ddsd.dwHeight = cy;

   // Create the surface
   CComPtr<IDirectDrawSurface> pIDDSTemp;
   HRESULT hr = m_pIDirectDraw->CreateSurface(&ddsd,&pIDDSTemp,NULL);
   if ( FAILED(hr) ) {
      // Some error
      *ppDDS = NULL;
      return hr;
   } // if

   // Return the surface
   *ppDDS = pIDDSTemp;
   (*ppDDS)->AddRef();

   return hr;
}



The first practical consideration you should note is that I encapsulated secondary surface creation within a single method. This makes sense because this code will be called over and over again as you load more and more bitmaps. The method is made to be general purpose by passing in the size of the surface to create. The second practical consideration is I that used ATL to manage my COM pointers. This also makes sense, as you will deal with many different COM pointers when working with DirectDraw. Using a smart pointer class such as CComPtr is only prudent, especially when you consider error conditions and handlers (did you release the pointer already or not when the error took place?).


Caution:  

Using the code in Listing 30.3, do not forget to AddRef() the outgoing interface pointer if you’re using ATL to contain the interface pointer for the object (surface) you created. The smart pointer will dutifully release the interface pointer when it leaves the scope of the helper function, thus providing you with an invalid pointer if you don’t tell the object to increment its interface count first.


Now that you have a helper function to create secondary surfaces, it’s time to turn to using it in conjunction with bitmaps. I’ll break bitmap loading into two steps. First, I’ll create the surface I require, and then I’ll copy the bitmap into the surface. I’ll do things this in this fashion because, if it’s done properly, I can reuse the copy function when realizing a new palette.

Loading the bitmap from the application’s resources is a simple matter, and when you have it loaded, it’s also easy to determine how large the bitmap is for surface creation. As you see in Listing 30.4, I use CBitmap::LoadBitmap() to retrieve the bitmap from the application’s resource pool. I then use the CBitmap::GetBitmap() method to retrieve a BITMAP structure, from which I determine the overall size of the bitmap. Given that, I use the secondary surface creation method from Listing 30.3 to create the surface I’ll associate with this bitmap. After the surface is created, I actually copy the bitmap to the surface using another helper function CopyDDBitmap(), which you see in Listing 30.5.

Listing 30.4 Loading a Bitmap for Use with DirectDraw


HRESULT CTableView::LoadDDBitmap(UINT nID,
ÄIDirectDrawSurface** ppDDS)
{
   // Check your pointer
   ASSERT(ppDDS != NULL);
   if ( ppDDS == NULL ) {
      // NULL input pointer...
      return E_POINTER;
   } // if

   // Pull the bitmap from resources
   CBitmap bmImage;
   VERIFY(bmImage.LoadBitmap(nID));

   // Determine its size
   BITMAP bm;
   bmImage.GetBitmap(&bm);

   // Create a surface to contain the bitmap
   CComPtr<IDirectDrawSurface> pIDDSTemp;
   HRESULT hr = CreateSecondarySurface(bm.bmWidth,bm.bmHeight,
                                       &pIDDSTemp);
   if ( FAILED(hr) ) {
      // Some error
      *ppDDS = NULL;
      return hr;
   } // if

   // Copy the bitmap to the surface
   hr = CopyDDBitmap(&bmImage,pIDDSTemp);
   if ( FAILED(hr) ) {
      // Some error
      *ppDDS = NULL;
      return hr;
   } // if

   // Return the surface
   *ppDDS = pIDDSTemp;
   (*ppDDS)->AddRef();

   return hr;
}

Listing 30.5 Copying a Bitmap into a DirectDraw Surface


HRESULT CTableView::CopyDDBitmap(CBitmap* pbmImage,
ÄIDirectDrawSurface* pDDS)
{
   // Restore the surface
   pDDS->Restore();

   // Create a memory DC to contain the bitmap
   CDC dcImage;
   dcImage.CreateCompatibleDC(NULL);
   CBitmap* pBitmapMono = (CBitmap*)dcImage.SelectObject(pbmImage);

   // Determine its size
   BITMAP bm;
   pbmImage->GetBitmap(&bm);

   // Complete the surface description
   DDSURFACEDESC ddsd;
   ddsd.dwSize = sizeof(ddsd);
   ddsd.dwFlags = DDSD_HEIGHT | DDSD_WIDTH;
   pDDS->GetSurfaceDesc(&ddsd);

   // Copy in the bitmap
   HDC hdc = NULL;
   HRESULT hr = pDDS->GetDC(&hdc);
   if ( SUCCEEDED(hr) ) {
      ::StretchBlt(hdc,0,0,ddsd.dwWidth,ddsd.dwHeight,
                   dcImage,0,0,bm.bmWidth,bm.bmHeight,SRCCOPY);
      pDDS->ReleaseDC(hdc);
   } // if

   // Clean up
   dcImage.SelectObject(pBitmapMono);

   return hr;
}

The bulk of the work loading bitmaps is done by CopyDDBitmap(). Here, I create a memory-based device context, into which I select the bitmap. Because DirectDraw surfaces must somehow marry their capabilities to Windows, it isn’t surprising to find the DirectDraw surface object supports device contexts. You simply ask the surface for a device context and use the standard API function StretchBlt() to copy the image from your memory device context to that of the surface. After the image has been copied, you release the surface’s device context (as you would normally do with device contexts), and then clean up the memory-based device context.

There is one detail I should also mention. Referring again to Listing 30.5, before I do anything with bitmaps, I call IDirectDrawSurface::Restore(). Surfaces, for a variety of reasons, could have their memory released yet still exist. This isn’t a problem or error condition—perhaps Windows required display memory for another application. In any case, calling Restore() will reallocate the amount of memory you originally requested, though all previous information will have been unrecoverably lost. This call simply assures you that the memory for your bitmap will be available when you attempt to copy it into place.

Palette Issues

In this section, I’ll revisit palette creation and look at the RePalettize() method I referred to previously in this chapter. Regarding palette creation, I’ll put all the pieces I discussed previously together in one method. I’ll then show you what it takes to re-establish a palette using DirectDraw.

Listing 30.6 shows you palette creation in a single method. The major difference between what you see in Listing 30.6 and the code snippets I provided when I discussed DirectDraw basics is the logic I use to return a valid palette no matter what I find in the bitmap. I also use ATL.

Listing 30.6 Creating a DirectDraw Palette


HRESULT CTableView::LoadDDPalette(UINT idBitmap,
ÄIDirectDrawPalette** ppDDP)
{
   // Create a standard 332 palette as the default
   PALETTEENTRY ape[256];
   for ( int i = 0; i < 256; i++ ) {
      ape[i].peRed   = (BYTE)(((i >> 5) & 0x07) * 255 / 7);
      ape[i].peGreen = (BYTE)(((i >> 2) & 0x07) * 255 / 7);
      ape[i].peBlue  = (BYTE)(((i >> 0) & 0x03) * 255 / 3);
      ape[i].peFlags = (BYTE)0;
   } // for

    // Locate the bitmap resource.  Note this assumes the
   // bitmap is stored as a program resource (and not as
   // a disk file).
   HRSRC hBitmap = NULL;
   if ( (hBitmap = ::FindResource(NULL,MAKEINTRESOURCE(idBitmap),
ÄRT_BITMAP))) {
      LPBITMAPINFOHEADER lpbi = (LPBITMAPINFOHEADER)
Ä::LockResource(::LoadResource(NULL,hBitmap));
      if ( lpbi != NULL ) {
         // Locate the color information, which is stored past
         // the BITMAPINFOHEADER.
         RGBQUAD* prgb = (RGBQUAD*)((BYTE*)lpbi + lpbi->biSize);

         // Determine how many palettized colors you’re
         // talking about.
         int iNumColors;
         if (lpbi == NULL || lpbi->biSize < sizeof(BITMAPINFOHEADER))
            iNumColors = 0;
         else if (lpbi->biBitCount > 8)
            iNumColors = 0;
         else if (lpbi->biClrUsed == 0)
            iNumColors = 1 << lpbi->biBitCount;
         else
            iNumColors = lpbi->biClrUsed;

         //  Pull the color information
         for( i = 0; i < iNumColors; i++ ) {
            ape[i].peRed   = prgb[i].rgbRed;
            ape[i].peGreen = prgb[i].rgbGreen;
            ape[i].peBlue  = prgb[i].rgbBlue;
            ape[i].peFlags = 0;
         } // for
      } // if
   } // if

   // Create a new DirectDraw palette
   CComPtr<IDirectDrawPalette> pIDDPTemp;
   HRESULT hr = m_pIDirectDraw->CreatePalette(DDPCAPS_8BIT,
Äape,&pIDDPTemp,NULL);
   if ( FAILED(hr) ) {
      // Some error
      *ppDDP = NULL;
      return hr;
   } // if

   // Return the palette
   *ppDDP = pIDDPTemp;
   (*ppDDP)->AddRef();

   return hr;
}



The goal is to create a palette no matter what happens to be contained within the bitmap I choose to provide to LoadDDPalette(). So, I begin by creating a 332 palette. Then, if I find invalid data stored within the bitmap, I simply create a palette with 332 data. On the other hand, if there is valid palette information contained within the bitmap, I replace the 332 palette color information with the bitmap data I found. In this way, I always create a valid palette unless the CreatePalette() method fails. In that case I have deeper problems than invalid palette color information.

Re-establishing a palette is a very simple matter, at least when using DirectDraw. In fact, the primary surface will have already handled the actual palette work for you on your behalf. What really remains to be done is to reload all your bitmaps. When you do, their individual color information will either be converted to the new palette (WM_PALETTECHANGED) or restored (WM_QUERYNEWPALETTE).

I chose to encapsulate this functionality in two helper functions, RePalettize() and ReLoadDDBitmap(). RePalettize() simply calls ReLoadDDBitmap() repetitively, once for each bitmap I use in my application. In the case of the BlackJack sample program, RePalettize() appears as you see in Listing 30.7.

Listing 30.7 BlackJack Repalettization


BOOL CTableView::RePalettize()
{
   // Reload our bitmaps...
   BOOL bReturn = TRUE;
   HRESULT hr = S_OK;
   try {
      hr = ReLoadDDBitmap(IDB_TABLE,m_lpDDSTable);
      if FAILED( hr ) {
         // Some error
         throw new CDDException(TRUE,“Error reloading table bitmap”);
      } // if

      hr = ReLoadDDBitmap(IDB_CARDS1,m_lpDDSCards1);
      if FAILED( hr ) {
         // Some error
         throw new CDDException(TRUE,
                   “Error reloading first card bitmap”);
      } // if

      hr = ReLoadDDBitmap(IDB_CARDS2,m_lpDDSCards2);
      if FAILED( hr ) {
         // Some error
         throw new CDDException(TRUE,
                   “Error reloading second card bitmap”);
      } // if

      hr = ReLoadDDBitmap(IDB_CARDBACK,m_lpDDSCardBack);
      if FAILED( hr ) {
         // Some error
         throw new CDDException(TRUE,
                   “Error reloading card back bitmap”);
      } // if
   } // try
   catch (CDDException* dde) {

      // Intercept your special exception and
      // tell user of error
      dde->ReportError(MB_OK | MB_ICONERROR,hr);

      // Delete the exception
      dde->Delete();

      // Return failure
      bReturn = FALSE;
   } // catch
   catch (...) {
      // Return failure
      bReturn = FALSE;
   } // catch

   return bReturn;
}

Here you see I call ReLoadDDBitmap() for each of the four bitmaps I use in the program. You also see how I handle DirectDraw errors, which I’ll discuss in detail later in the chapter. For now, though, you can see I use C++ exception handling with a custom exception class.

The ReLoadDDBitmap() method is quite simplistic because the hard work is performed in CopyDDBitmap(), as you see in Listing 30.8. All that is required is to copy the bitmap associated with a given surface back into the surface. This is one major reason why CopyDDBitmap() calls the IDirectDrawSurface::Restore() method. In any case, merely copying the bitmap back into the surface causes DirectDraw to adjust the display of the bitmap appropriately no matter if the application is gaining or losing the focus (using its own palette or another application’s). If you’ve never dealt with palettes before in your application development, you might believe that all this code to support the system palette is somewhat of a burden. However, those of you who have worked with palettes before will probably agree that this is a painless and simple way to be sure your application appears as it was intended. DirectDraw is certainly the major reason this is so simple to implement.

Listing 30.8 Reloading DirectDraw Bitmaps


HRESULT CTableView::ReLoadDDBitmap(UINT nID, IDirectDrawSurface* pDDS)
{
   // Pull the bitmap from resources
   CBitmap bmImage;
   VERIFY(bmImage.LoadBitmap(nID));

   // Copy the bitmap back to the surface
   return CopyDDBitmap(&bmImage,pDDS);
}

Windowed Rendering

As I mentioned previously, those applications that want to use DirectDraw from within a window have the additional requirement of creating a clipper object and associating it with their primary surface. But it is also true that the window might be resized at any time, thus changing the rules of the game for the clipper. It has to re-establish any and all clipping regions, thus allowing your application to repaint itself properly when later obscured by other windows.

You might believe this would be done while handling the WM_SIZE message, and you certainly could handle matters there. However, I find it more useful to encapsulate resizing and repainting in a single method. After all, a WM_SIZE message will most certainly result in a WM_PAINT message, so you can handle both issues at the time you repaint. Therefore, I’ve added a WM_PAINT handler to the BlackJack sample program (see Listing 30.9), and in the handler I call another helper function, Blt(), which you see in Listing 30.10.

Listing 30.9 BlackJack WM_PAINT Handler


void CTableView::OnPaint()
{
   CPaintDC dc(this); // device context for painting

   // Copy our secondary buffer to primary
   Blt();

   // Do not call CWnd::OnPaint() for painting messages
}

Listing 30.10 DirectDraw Windowed Blt() Method


HRESULT CTableView::Blt()
{
   CRect rcClient;
   GetClientRect(rcClient);
   ClientToScreen(rcClient);
   HRESULT hr = m_lpDDSPrimary->Blt(rcClient,m_lpDDSSecondary.p,NULL,
                    DDBLT_WAIT,NULL);
   if ( FAILED(hr) ) {
      AfxMessageBox(“Error bltting primary surface”,
                     MB_OK | MB_ICONERROR);
   } // if

   return hr;
}

All that is required is to simply supply the IDirectDraw::Blt() method with the current client area rectangle in screen coordinates. When the blit takes place, the IDirectDraw::Blt() makes sure to honor the rectangle you provided. Note that I elected not to use C++ exception handling in this case as I was making a single DirectDraw method call. In fact, this is a good time to discuss DirectDraw error handling in general.

Error Handling

The impetus for handling errors in the first place is to eliminate the problems associated with cascading return codes. By this I mean I want to avoid the following code situation:

HRESULT hr = pSomeObject->SomeFunction();
if ( SUCCEEDED(hr) ) {
   hr = pSomeObject->AnotherFunction();
   if ( SUCCEEDED(hr) ) {
      hr = pSomeObject->AThirdFunction();
      if ( SUCCEEDED(hr) ) {
         hr = pSomeObject->AUsefulFunction();
         if ( SUCCEEDED(hr) ) {
            // Do something interesting...
         } // if
      } // if
   } // if
} // if

As with many COM objects, such error checking can get nested many levels deep very quickly. Another way to look at this same issue is the multiple return-point situation:

HRESULT hr = pSomeObject->SomeFunction();
if ( FAILED(hr) ) {
   AfxMessageBox(“I failed”);
   return hr;
} // if

hr = pSomeObject->AnotherFunction();
if (FAILED (hr) ) {
   AfxMessageBox(“I failed”);
   return hr;
} // if

hr = pSomeObject->AThirdFunction();
if (FAILED (hr) ) {
   AfxMessageBox(“I failed”);
   return hr;
} // if

hr = pSomeObject->AUsefulFunction();
if (FAILED (hr) ) {
   AfxMessageBox(“I failed”);
   return hr;
} // if

// Do something interesting...



Multiple return points present a problem when deciding how to clean up after an error has occurred. You’re faced with making a determination regarding what happened and what needs to be done to recover from the error. The tricky part is that you might not know precisely where the error happened, making it difficult to clean up and/or recover.

C++ exception handling is a clean and elegant way to avoid problems such as these, which crop up often when working with COM (which is why the topic is reiterated here with a COM flavor). If you’re interested, be sure to read all about C++ exception handling in general in Chapter 22, “Exceptions.” Handling exceptions makes error recovery much easier because the error can be managed within the scope of the erroneous condition. At this time, local variables are still within scope and may be queried or reset as required to recover. An added benefit is that there is no need to repeat the same or similar error code, as is the case with the multiple return point example. You put all the error-handling code into the exception handler (the catch block) and manage the error locally. If you want to pass the error condition on, you can do so, or you can retry the operation to see if the error condition has been cleared.

Using the same code snippets as an example, here is my contrived example using exception handling:

HRESULT hr = S_OK;
try {
   pSomeObject->SomeFunction();
   if ( FAILED(hr) ) {
      throw new CMyException();
   } // if

   hr = pSomeObject->AnotherFunction();
   if (FAILED (hr) ) {
      throw new CMyException();
   } // if

   hr = pSomeObject->AThirdFunction();
   if (FAILED (hr) ) {
      throw new CMyException();
   } // if

   hr = pSomeObject->AUsefulFunction();
   if (FAILED (hr) ) {
      throw new CMyException();
} // if

   // Do something interesting...
} // try
catch (CMyException* e) {
   e->Delete() // assuming CException-based
   AfxMessageBox(“I failed”);
   // Handle error
   hr = S_OK;
} // catch
catch (...) {
   AfxMessageBox(“I failed”);
   // Handle error
   hr = S_OK;
} // catch

return hr;

From a line count perspective, there is more code when using exceptions. However, program operation is enhanced because error handling is located in a single location, where you’re better able to determine what should be done given the current error condition.

If you re-examine Listing 30.7, you’ll see that this example and the code shown in the listing are very similar. This isn’t coincidental. In the case of the BlackJack program, I merely give up, display an error message box, and punt. In general, though, more could be done if you were to examine the error code. Perhaps the error is recoverable.

Referring again to Listing 30.7, you see I created a CDDException object when I threw the exception. This exception object, as it happens, is particularly useful when working with DirectDraw (and DirectX in general). The reason for this is that DirectDraw (and DirectX) define custom return codes. When you’re developing DirectDraw applications, you’ll no doubt receive errors from the COM objects for one reason or another. When you do, you can’t use the handy error lookup tool from Developer Studio to view a textual representation of the error. The tool won’t find DirectDraw error codes. Therefore, I implemented a similar feature in CDDException. You will see the error message you pass in displayed in the first line of the error message box. However, the second line will contain a textual representation of the error from DirectDraw. Chapter 22 has the details regarding creating custom exception classes. In this case, CDDException is based upon CException and implements custom GetErrorMessage() and ReportError() methods. Be sure to look at DDError.cpp and DDError.h in the BlackJack project if you’re interested.

DirectDraw Shutdown

The final topic for this chapter is to look at the proper way to shut down your application when using DirectDraw. If you use ATL, you might believe you don’t need to worry about releasing the DirectDraw COM objects. And from a COM perspective, you would be correct. However, there is an order in which the objects must be released. If you fail to follow this order, you will most certainly receive access violations further on down the road as your application continues to close.

Listing 30.11 shows you BlackJack’s WM_DESTROY message handler. The correct order to release the DirectDraw objects is

1.  Release the primary surface.
2.  Release any secondary surfaces.
3.  Release the palette object.
4.  Release the DirectDraw object itself.

As a side note, releasing the primary surface also releases any clipper object you might have previously associated with your application’s window.

An interesting feature when using ATL to encapsulate your COM interface pointers is that you don’t need to call Release() directly. All you need to do is simply set the CComPtr variable to NULL, and ATL will handle the Release() for you. You also see this in action in Listing 30.11.

Listing 30.11 Releasing BlackJack DirectDraw Objects


void CTableView::OnDestroy()
{
   // Release your surfaces first.  The order in which
   // DirectDraw objects are released is important.
   // Surfaces first, then the palette, then finally
   // the DirectDraw object.
   m_lpDDSPrimary = NULL;
   m_lpDDSSecondary = NULL;
   m_lpDDSCards1 = NULL;
   m_lpDDSCards2 = NULL;
   m_lpDDSCardBack = NULL;
   m_lpDDSTable = NULL;

   // Now the palette.  If you never created one,
   // this call will still succeed (thanks to
   // CComPtr).
   m_lpDDPalette = NULL;

   // Finally release your DirectDraw object
   m_pIDirectDraw = NULL;

   CWnd::OnDestroy();
}

Remember to reverse the order of primary and secondary (back buffer) surface release calls when using page flipping. In this case, the secondary surface is released before the primary, as both the primary and secondary surfaces are, in fact, the same surface. If you fail to follow this order of release, you will most certainly face an access violation when your application attempts to release the DirectDraw objects. Listing 30.12 shows you how this is done in the tearing demo. Here, I keep track of what mode was used to initialize the program and take the proper steps to release the DirectDraw objects accordingly.

Listing 30.12 Releasing Tearing Demo DirectDraw Objects


void CChildView::ReleaseDDObjects()
{
   // Release your surfaces first.  The order in which
   // DirectDraw objects are released is important.
   // Surfaces first, then the palette, then finally
   // the DirectDraw object.
   if ( !m_bFlip ) {
      // When you’re not page flipping, we release the
      // DirectDraw objects in the normal order.
      m_lpDDSPrimary = NULL;
      m_lpDDSSecondary = NULL;
   } // if
   else {
      // When you’re page flipping, you reverse the order
      // of release for the primary and secondary
      // surfaces.  This is because they really are
      // the SAME surface and must be released in an
      // order opposite to that which created them.
      m_lpDDSSecondary = NULL;
      m_lpDDSPrimary = NULL;
   } // else
   m_lpDDSCan = NULL;

   // Now the palette.  If you never created one,
   // this call will still succeed (thanks to
   // CComPtr).
   m_lpDDPalette = NULL;

   // Finally release your DirectDraw object
   m_pIDirectDraw = NULL;
}

Summary

In this chapter, you were introduced to DirectX and saw quite a bit of detail regarding DirectDraw. Armed with this information, you have what you require to add stunning graphics to your applications with surprising ease. Don’t let the BlackJack sample fool you—you can use DirectDraw (and DirectX) in any application, not just games. On the other hand, if you have a game in mind...